import numpy as np
import scipy.special

class NeuralNetwork:

    def __init__(self,inputnodes,hiddennodes,outputnodes,learningrate):
        '''
        设定输入层节点、隐藏层节点和输出层节点的数量；
        创建链接权重矩阵
        :param inputnodes:输入节点数量
        :param hiddennodes: 隐藏节点数量
        :param outputnodes: 输出节点数量
        :param learningrate: 学习率
        '''
        self.inodes=inputnodes
        self.hnodes=hiddennodes
        self.onodes=outputnodes

        self.lr=learningrate
        # 创建两个链接权重矩阵，权重数为随机值，数组中的权重为wij，其链接是从节点i到下一层的节点j，rand函数生成0~1之间的随机数
        # -0.5是为了将数组变为正负0.5之间的随机数
        # wih是从输入层到隐藏层的链接权重
        # who是从隐藏层到输出层的链接权重
        self.wih=np.random.rand(self.hnodes,self.inodes)-0.5
        self.who=np.random.rand(self.onodes,self.hnodes)-0.5
        # 以下为使用正态概率分布采样权重
        # self.wih=np.random.normal(0.0,pow(self.hnodes,-0.5),(self.hnodes,self.inodes))
        # self.who=np.random.normal(0.0,pow(self.onodes,-0.5),(self.onodes,self.hnodes))
        # 激活函数
        self.activation_function=lambda x:scipy.special.expit(x)

    def train(self,inputs_list,targets_list):
        '''
        学习给定训练集样本后，优化权重
        训练任务分为两步
        1. 针对给定的训练样本计算输出。这与query函数上做的没什么区别
        2. 将计算得到的输出与所需输出对比，使用误差值来指导网络权重的更新
        :param inputs_list:
        :param targets_list:
        :return:
        '''
        # 第一步，与query函数相同操作
        inputs=np.array(inputs_list,ndmin=2).T
        # 由于需要使用包含期望值或目标答案的训练样本来训练网络。因此对目标值target_list进行了初步处理
        targets=np.array(targets_list,ndmin=2).T
        hidden_inputs=np.dot(self.wih,inputs)
        hidden_outputs=self.activation_function(hidden_inputs)
        final_inputs=np.dot(self.who,hidden_outputs)
        final_outputs=self.activation_function(final_inputs)
        # 计算误差，即训练样本所提供的预期目标输出值与实际计算得到的输出值之差；也即将矩阵targets和矩阵
        # final_outputs中每个对应元素相减得到的。使用Python进行矩阵相减
        output_errors=targets-final_outputs
        # 计算出隐藏层节点反向传播的误差。
        # 根据隐藏层误差计算公式:隐藏层误差=隐藏层输出的矩阵反转*输出误差
        hidden_errors=np.dot(self.who.T,output_errors)
        # 更新隐藏层和输出层链接的权重
        self.who+=self.lr*np.dot((output_errors*
                                  final_outputs*(1.0-final_outputs)),
                                 np.transpose(hidden_outputs))
        # 更新输入层和隐藏层链接的权重
        self.wih+=self.lr*np.dot((hidden_errors*hidden_outputs*(1.0-hidden_outputs))
                                 ,np.transpose(inputs))


    def query(self,inputs_list):
        '''
        给定输入，通过矩阵点乘计算输入值，并对输入值调用激活函数产生输出，隐藏层与输出层执行同样操作。
        返回输出层结果
        :param inputs_list: 输入值
        :return:
        '''
        # 将输入转置为二维数组，ndmin参数表示数组最少维度数，T表示转置
        inputs=np.array(inputs_list,ndmin=2).T
        # 对权重矩阵和输入矩阵使用点乘法生成新的矩阵作为隐藏层的输入
        # 根据公式：
        hidden_inputs=np.dot(self.wih,inputs)
        # 对隐藏层输入矩阵调用激活函数
        hidden_outputs=self.activation_function(hidden_inputs)
        # 隐藏层权重矩阵与藏层输出矩阵调用点乘法
        final_inputs=np.dot(self.who,hidden_outputs)
        # 对输出层输入矩阵调用激活函数
        final_outputs=self.activation_function(final_inputs)
        # 返回输出层输出结果
        return final_outputs

# if __name__=='__main__':
#     # 输入层、隐藏层和输出层三层，每层3个节点
#     input_nodes=3
#     hidden_nodes=3
#     output_nodes=3
#     # 设置学习率为0.3
#     learn_rate=0.3
#     n=NeuralNetwork(input_nodes,hidden_nodes,output_nodes,learn_rate)
#     output=n.query([1.0,0.5,-1.5])
#     print(output)